Skip to content

Android: fix controller rumble motor separation via VibratorManager (API 31+)#18906

Merged
LibretroAdmin merged 3 commits intolibretro:masterfrom
TideGear:DualShock-Fix
Apr 7, 2026
Merged

Android: fix controller rumble motor separation via VibratorManager (API 31+)#18906
LibretroAdmin merged 3 commits intolibretro:masterfrom
TideGear:DualShock-Fix

Conversation

@TideGear
Copy link
Copy Markdown
Contributor

@TideGear TideGear commented Apr 7, 2026

Android's joypad rumble backend has been collapsing both libretro motor channels into a single vibration output since its introduction. android_input_set_rumble_internal OR-merged RETRO_RUMBLE_STRONG and RETRO_RUMBLE_WEAK amplitudes before forwarding them to InputDevice.getVibrator() — a single-vibrator API with no concept of individual motors. The effect type was discarded at the JNI call site, so every rumble event regardless of channel drove the same motor at the same merged strength. Weak and strong rumble were completely indistinguishable on any dual-motor controller.

This patch introduces a new doVibrateJoypad JNI method that accepts both channels separately and uses InputDevice.getVibratorManager() on Android 12+ (API 31) to drive each controller motor independently.

Changes:

  • android_joypad.c: Remove the OR-merge of strong | weak. Each channel is updated independently in the existing per-port state arrays. When doVibrateJoypad is registered and id >= 0, both normalised amplitudes (0–255) are passed to Java separately. The device vibration path (id == -1) and the legacy OR-merge fallback are preserved exactly.
  • RetroActivityCommon.java: Add doVibrateJoypad(int id, int strong, int weak, int unused). On Android 12+, doVibrateJoypadApi31 uses InputDevice.getVibratorManager() to enumerate vibrator IDs and drives them independently via CombinedVibration.startParallel(). Index 0 → strong (large/low-freq), index 1 → weak (small/high-freq); consistent across all tested controllers but not guaranteed by the Android API and documented as such. If only one vibrator is exposed, max(strong, weak) is used. If zero vibrators are exposed or on Android < 12, returns false and doVibrate() is called as before. doVibrate() itself is untouched.
  • platform_unix.h / platform_unix.c: Register doVibrateJoypad alongside doVibrate via GET_METHOD_ID. Clears exceptions on failure so doVibrateJoypad is safely NULL on any build without the new Java method, causing the C-side fallback to fire automatically.
  • build.gradle: compileSdkVersion bumped 30 → 31, required to reference VibratorManager and CombinedVibration at compile time. minSdkVersion unchanged at API 16.

Compatibility:

  • Android < 12 falls back to the existing doVibrate() single-vibrator behaviour.
  • "Enable Device Vibration" (id == -1) is unaffected.
  • No deprecated Android APIs introduced.
  • C changes are C89 and ISO C++ compatible. Verified with C89_BUILD=1.
  • No new -Wall warnings introduced.

Tested on Android 14:

  • Sony DualShock 4 (Bluetooth)
  • Sony DualSense (Bluetooth)
  • 8BitDo Pro 2 in Xbox One mode (Bluetooth)

Tested with SwanStation in Metal Gear Solid's options. Weak and strong rumbles now behave properly. They were previously indistinguishable.

Related Issues

libretro/swanstation#72 — open since May 18, 2023

Related Pull Requests

None.

Reviewers

Please check git log --follow input/drivers_joypad/android_joypad.c for recent contributors to this area of the codebase.

TideGear added 3 commits April 6, 2026 21:42
…API 31+)

RetroArch Android collapsed both libretro rumble channels (RETRO_RUMBLE_STRONG
and RETRO_RUMBLE_WEAK) into a single vibration output by OR-merging their
amplitudes before calling InputDevice.getVibrator(). This made the strong
(large/low-frequency) and weak (small/high-frequency) motors feel identical —
rendering motor separation in cores like SwanStation completely ineffective.

Fixes libretro/swanstation#72.

Changes
-------

android_joypad.c:
- Remove the OR-merge of strong | weak into new_strength. Each channel is now
  updated independently and held in the existing last_strength_strong /
  last_strength_weak per-port state.
- Add a new JNI call path to doVibrateJoypad when the method is registered and
  id >= 0 (controller, not device vibration). Both normalized amplitudes are
  passed separately so the Java side can route them to individual motors.
- Preserve the original OR-merge behavior as the legacy fallback for the device
  vibration path (id == -1) and for builds where doVibrateJoypad is unavailable.

RetroActivityCommon.java:
- Add doVibrateJoypad(int id, int strongStrength, int weakStrength, int unused),
  the new JNI entry point for per-channel controller rumble.
- On Android 12+ (API 31), doVibrateJoypadApi31() uses
  InputDevice.getVibratorManager() to enumerate the controller's vibrator IDs
  and drives them independently via CombinedVibration.startParallel(). Index 0
  is mapped to the strong (large/low-freq) motor and index 1 to the weak
  (small/high-freq) motor. This ordering is consistent across tested controllers
  but is not guaranteed by the Android API; the code degrades gracefully.
- If the controller exposes only one vibrator, max(strong, weak) is used so the
  device still rumbles.
- If VibratorManager returns no vibrator IDs, or on Android < 12, falls back to
  the existing doVibrate() single-vibrator path.
- The existing doVibrate() method and the "Enable Device Vibration" path are
  completely unchanged.

platform_unix.h / platform_unix.c:
- Register doVibrateJoypad as a new jmethodID alongside doVibrate.
  GET_METHOD_ID clears exceptions on lookup failure, so doVibrateJoypad is NULL
  on older APKs and the C-side fallback fires automatically.

build.gradle:
- Bump compileSdkVersion from 30 to 31. Required to reference VibratorManager
  and CombinedVibration at compile time. minSdkVersion is unchanged (API 16).

Tested
------

Confirmed strong and weak motors are now driven independently on Android 14+:
- Sony DualShock 4 (Bluetooth)
- Sony DualSense (Bluetooth)
- 8BitDo Pro 2 in Xbox One mode (Bluetooth)

Verified in SwanStation running Metal Gear Solid (PS1): weak-only rumble events
(e.g. distant explosions) are now perceptibly lighter than strong-only events
(e.g. alert state), which were previously indistinguishable.

Fallback behavior confirmed unaffected: controllers on Android < 12 still
rumble via the legacy single-vibrator path, and "Enable Device Vibration"
continues to function as before.
…API 31+)

RetroArch Android collapsed both libretro rumble channels (RETRO_RUMBLE_STRONG
and RETRO_RUMBLE_WEAK) into a single vibration output by OR-merging their
amplitudes before calling InputDevice.getVibrator(). The effect type was
discarded at the JNI call site, making weak and strong rumble completely
indistinguishable on dual-motor controllers.

Introduces doVibrateJoypad, a new JNI method that accepts both channels
separately. On Android 12+ (API 31) it uses InputDevice.getVibratorManager()
to enumerate controller vibrator IDs and drives each motor independently via
CombinedVibration.startParallel(). Index 0 maps to the strong (large/low-freq)
motor, index 1 to the weak (small/high-freq) motor — consistent across tested
controllers but not guaranteed by the Android API; documented as a best-effort
heuristic. Falls back to the existing single-vibrator doVibrate() path on
Android < 12, single-vibrator controllers, or if doVibrateJoypad is not found
at JNI lookup time. The doVibrate() method and "Enable Device Vibration" path
are entirely unchanged.

compileSdkVersion bumped 30 -> 31 (minimum required for VibratorManager and
CombinedVibration at compile time; minSdkVersion unchanged at API 16). No
deprecated APIs introduced. C changes are C89 and ISO C++ compatible.

Fixes libretro/swanstation#72.

Tested on Android 14:
- Sony DualShock 4 (Bluetooth and USB)
- Sony DualSense (Bluetooth and USB)
- 8BitDo Pro 2 in Xbox One mode (Bluetooth)
@LibretroAdmin LibretroAdmin merged commit 9cee488 into libretro:master Apr 7, 2026
35 checks passed
@TideGear TideGear deleted the DualShock-Fix branch April 7, 2026 06:34
@TideGear
Copy link
Copy Markdown
Contributor Author

TideGear commented Apr 8, 2026

Note! This only fixes the vibration in Bluetooth mode! Working on a USB fix too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants